/* @internal */
namespace ts.SignatureHelp {
    const enum InvocationKind { Call, TypeArgs, Contextual }
    interface CallInvocation { readonly kind: InvocationKind.Call; readonly node: CallLikeExpression; }
    interface TypeArgsInvocation { readonly kind: InvocationKind.TypeArgs; readonly called: Identifier; }
    interface ContextualInvocation {
        readonly kind: InvocationKind.Contextual;
        readonly signature: Signature;
        readonly node: Node; // Just for enclosingDeclaration for printing types
        readonly symbol: Symbol;
    }
    type Invocation = CallInvocation | TypeArgsInvocation | ContextualInvocation;

    interface ArgumentListInfo {
        readonly isTypeParameterList: boolean;
        readonly invocation: Invocation;
        readonly argumentsSpan: TextSpan;
        readonly argumentIndex: number;
        /** argumentCount is the *apparent* number of arguments. */
        readonly argumentCount: number;
    }

    export function getSignatureHelpItems(program: Program, sourceFile: SourceFile, position: number, triggerReason: SignatureHelpTriggerReason | undefined, cancellationToken: CancellationToken): SignatureHelpItems | undefined {
        const typeChecker = program.getTypeChecker();

        // Decide whether to show signature help
        const startingToken = findTokenOnLeftOfPosition(sourceFile, position);
        if (!startingToken) {
            // We are at the beginning of the file
            return undefined;
        }

        // Only need to be careful if the user typed a character and signature help wasn't showing.
        const onlyUseSyntacticOwners = !!triggerReason && triggerReason.kind === "characterTyped";

        // Bail out quickly in the middle of a string or comment, don't provide signature help unless the user explicitly requested it.
        if (onlyUseSyntacticOwners && (isInString(sourceFile, position, startingToken) || isInComment(sourceFile, position))) {
            return undefined;
        }

        const isManuallyInvoked = !!triggerReason && triggerReason.kind === "invoked";
        const argumentInfo = getContainingArgumentInfo(startingToken, position, sourceFile, typeChecker, isManuallyInvoked);
        if (!argumentInfo) return undefined;

        cancellationToken.throwIfCancellationRequested();

        // Extra syntactic and semantic filtering of signature help
        const candidateInfo = getCandidateOrTypeInfo(argumentInfo, typeChecker, sourceFile, startingToken, onlyUseSyntacticOwners);
        cancellationToken.throwIfCancellationRequested();

        if (!candidateInfo) {
            // We didn't have any sig help items produced by the TS compiler.  If this is a JS
            // file, then see if we can figure out anything better.
            return undefined;
        }

        return typeChecker.runWithCancellationToken(cancellationToken, typeChecker =>
            candidateInfo.kind === CandidateOrTypeKind.Candidate
                ? createSignatureHelpItems(candidateInfo.candidates, candidateInfo.resolvedSignature, argumentInfo, sourceFile, typeChecker)
                : createTypeHelpItems(candidateInfo.symbol, argumentInfo, sourceFile, typeChecker));
    }

    const enum CandidateOrTypeKind { Candidate, Type }
    interface CandidateInfo {
        readonly kind: CandidateOrTypeKind.Candidate;
        readonly candidates: readonly Signature[];
        readonly resolvedSignature: Signature;
    }
    interface TypeInfo {
        readonly kind: CandidateOrTypeKind.Type;
        readonly symbol: Symbol;
    }

    function getCandidateOrTypeInfo({ invocation, argumentCount }: ArgumentListInfo, checker: TypeChecker, sourceFile: SourceFile, startingToken: Node, onlyUseSyntacticOwners: boolean): CandidateInfo | TypeInfo | undefined {
        switch (invocation.kind) {
            case InvocationKind.Call: {
                if (onlyUseSyntacticOwners && !isSyntacticOwner(startingToken, invocation.node, sourceFile)) {
                    return undefined;
                }
                const candidates: Signature[] = [];
                const resolvedSignature = checker.getResolvedSignatureForSignatureHelp(invocation.node, candidates, argumentCount)!; // TODO: GH#18217
                return candidates.length === 0 ? undefined : { kind: CandidateOrTypeKind.Candidate, candidates, resolvedSignature };
            }
            case InvocationKind.TypeArgs: {
                const { called } = invocation;
                if (onlyUseSyntacticOwners && !containsPrecedingToken(startingToken, sourceFile, isIdentifier(called) ? called.parent : called)) {
                    return undefined;
                }
                const candidates = getPossibleGenericSignatures(called, argumentCount, checker);
                if (candidates.length !== 0) return { kind: CandidateOrTypeKind.Candidate, candidates, resolvedSignature: first(candidates) };

                const symbol = checker.getSymbolAtLocation(called);
                return symbol && { kind: CandidateOrTypeKind.Type, symbol };
            }
            case InvocationKind.Contextual:
                return { kind: CandidateOrTypeKind.Candidate, candidates: [invocation.signature], resolvedSignature: invocation.signature };
            default:
                return Debug.assertNever(invocation);
        }
    }

    function isSyntacticOwner(startingToken: Node, node: CallLikeExpression, sourceFile: SourceFile): boolean {
        if (!isCallLikeExpression(node)) return false;
        const invocationChildren = node.getChildren(sourceFile);
        switch (startingToken.kind) {
            case SyntaxKind.OpenParenToken:
                return contains(invocationChildren, startingToken);
            case SyntaxKind.CommaToken: {
                const containingList = findContainingList(startingToken);
                return !!containingList && contains(invocationChildren, containingList);
            }
            case SyntaxKind.LessThanToken:
                return containsPrecedingToken(startingToken, sourceFile, node.expression);
            default:
                return false;
        }
    }

    function containsPrecedingToken(startingToken: Node, sourceFile: SourceFile, container: Node) {
        const pos = startingToken.getFullStart();
        // There’s a possibility that `startingToken.parent` contains only `startingToken` and
        // missing nodes, none of which are valid to be returned by `findPrecedingToken`. In that
        // case, the preceding token we want is actually higher up the tree—almost definitely the
        // next parent, but theoretically the situation with missing nodes might be happening on
        // multiple nested levels.
        let currentParent: Node | undefined = startingToken.parent;
        while (currentParent) {
            const precedingToken = findPrecedingToken(pos, sourceFile, currentParent, /*excludeJsdoc*/ true);
            if (precedingToken) {
                return rangeContainsRange(container, precedingToken);
            }
            currentParent = currentParent.parent;
        }
        return Debug.fail("Could not find preceding token");
    }

    export interface ArgumentInfoForCompletions {
        readonly invocation: CallLikeExpression;
        readonly argumentIndex: number;
        readonly argumentCount: number;
    }
    export function getArgumentInfoForCompletions(node: Node, sourceFile: SourceFile): ArgumentInfoForCompletions | undefined {
        const info = getImmediatelyContainingArgumentInfo(node, sourceFile);
        return !info || info.isTypeParameterList || info.invocation.kind !== InvocationKind.Call ? undefined
            : { invocation: info.invocation.node, argumentCount: info.argumentCount, argumentIndex: info.argumentIndex };
    }

    function getArgumentOrParameterListInfo(node: Node, sourceFile: SourceFile): { readonly list: Node, readonly argumentIndex: number, readonly argumentCount: number, readonly argumentsSpan: TextSpan } | undefined {
        const info = getArgumentOrParameterListAndIndex(node, sourceFile);
        if (!info) return undefined;
        const { list, argumentIndex } = info;

        const argumentCount = getArgumentCount(list);
        if (argumentIndex !== 0) {
            Debug.assertLessThan(argumentIndex, argumentCount);
        }
        const argumentsSpan = getApplicableSpanForArguments(list, sourceFile);
        return { list, argumentIndex, argumentCount, argumentsSpan };
    }
    function getArgumentOrParameterListAndIndex(node: Node, sourceFile: SourceFile): { readonly list: Node, readonly argumentIndex: number } | undefined {
        if (node.kind === SyntaxKind.LessThanToken || node.kind === SyntaxKind.OpenParenToken) {
            // Find the list that starts right *after* the < or ( token.
            // If the user has just opened a list, consider this item 0.
            return { list: getChildListThatStartsWithOpenerToken(node.parent, node, sourceFile), argumentIndex: 0 };
        }
        else {
            // findListItemInfo can return undefined if we are not in parent's argument list
            // or type argument list. This includes cases where the cursor is:
            //   - To the right of the closing parenthesis, non-substitution template, or template tail.
            //   - Between the type arguments and the arguments (greater than token)
            //   - On the target of the call (parent.func)
            //   - On the 'new' keyword in a 'new' expression
            const list = findContainingList(node);
            return list && { list, argumentIndex: getArgumentIndex(list, node) };
        }
    }

    /**
     * Returns relevant information for the argument list and the current argument if we are
     * in the argument of an invocation; returns undefined otherwise.
     */
    function getImmediatelyContainingArgumentInfo(node: Node, sourceFile: SourceFile): ArgumentListInfo | undefined {
        const { parent } = node;
        if (isCallLikeExpression(parent)) {
            const invocation = parent;

            // There are 3 cases to handle:
            //   1. The token introduces a list, and should begin a signature help session
            //   2. The token is either not associated with a list, or ends a list, so the session should end
            //   3. The token is buried inside a list, and should give signature help
            //
            // The following are examples of each:
            //
            //    Case 1:
            //          foo<#T, U>(#a, b)    -> The token introduces a list, and should begin a signature help session
            //    Case 2:
            //          fo#o<T, U>#(a, b)#   -> The token is either not associated with a list, or ends a list, so the session should end
            //    Case 3:
            //          foo<T#, U#>(a#, #b#) -> The token is buried inside a list, and should give signature help
            // Find out if 'node' is an argument, a type argument, or neither
            const info = getArgumentOrParameterListInfo(node, sourceFile);
            if (!info) return undefined;
            const { list, argumentIndex, argumentCount, argumentsSpan } = info;
            const isTypeParameterList = !!parent.typeArguments && parent.typeArguments.pos === list.pos;
            return { isTypeParameterList, invocation: { kind: InvocationKind.Call, node: invocation }, argumentsSpan, argumentIndex, argumentCount };
        }
        else {
            const typeArgInfo = getPossibleTypeArgumentsInfo(node, sourceFile);
            if (typeArgInfo) {
                const { called, nTypeArguments } = typeArgInfo;
                const invocation: Invocation = { kind: InvocationKind.TypeArgs, called };
                const argumentsSpan = createTextSpanFromBounds(called.getStart(sourceFile), node.end);
                return { isTypeParameterList: true, invocation, argumentsSpan, argumentIndex: nTypeArguments, argumentCount: nTypeArguments + 1 };
            }
            return undefined;
        }
    }

    function getImmediatelyContainingArgumentOrContextualParameterInfo(node: Node, position: number, sourceFile: SourceFile, checker: TypeChecker): ArgumentListInfo | undefined {
        return tryGetParameterInfo(node, position, sourceFile, checker) || getImmediatelyContainingArgumentInfo(node, sourceFile);
    }

    function getHighestBinary(b: BinaryExpression): BinaryExpression {
        return isBinaryExpression(b.parent) ? getHighestBinary(b.parent) : b;
    }

    function countBinaryExpressionParameters(b: BinaryExpression): number {
        return isBinaryExpression(b.left) ? countBinaryExpressionParameters(b.left) + 1 : 2;
    }

    function tryGetParameterInfo(startingToken: Node, _position: number, sourceFile: SourceFile, checker: TypeChecker): ArgumentListInfo | undefined {
        const info = getContextualSignatureLocationInfo(startingToken, sourceFile, checker);
        if (!info) return undefined;
        const { contextualType, argumentIndex, argumentCount, argumentsSpan } = info;

        const signatures = contextualType.getCallSignatures();
        if (signatures.length !== 1) return undefined;

        const invocation: ContextualInvocation = { kind: InvocationKind.Contextual, signature: first(signatures), node: startingToken, symbol: chooseBetterSymbol(contextualType.symbol) };
        return { isTypeParameterList: false, invocation, argumentsSpan, argumentIndex, argumentCount };
    }

    interface ContextualSignatureLocationInfo {readonly contextualType: Type; readonly argumentIndex: number; readonly argumentCount: number; readonly argumentsSpan: TextSpan; }
    function getContextualSignatureLocationInfo(startingToken: Node, sourceFile: SourceFile, checker: TypeChecker): ContextualSignatureLocationInfo | undefined {
        if (startingToken.kind !== SyntaxKind.OpenParenToken && startingToken.kind !== SyntaxKind.CommaToken) return undefined;
        const { parent } = startingToken;
        switch (parent.kind) {
            case SyntaxKind.ParenthesizedExpression:
            case SyntaxKind.FunctionExpression:
            case SyntaxKind.ArrowFunction:
                const info = getArgumentOrParameterListInfo(startingToken, sourceFile);
                if (!info) return undefined;
                const { argumentIndex, argumentCount, argumentsSpan } = info;
                const contextualType = checker.getContextualType(parent as ParenthesizedExpression | FunctionExpression | ArrowFunction);
                return contextualType && { contextualType, argumentIndex, argumentCount, argumentsSpan };
            case SyntaxKind.BinaryExpression: {
                const highestBinary = getHighestBinary(parent as BinaryExpression);
                const contextualType = checker.getContextualType(highestBinary);
                const argumentIndex = startingToken.kind === SyntaxKind.OpenParenToken ? 0 : countBinaryExpressionParameters(parent as BinaryExpression) - 1;
                const argumentCount = countBinaryExpressionParameters(highestBinary);
                return contextualType && { contextualType, argumentIndex, argumentCount, argumentsSpan: createTextSpanFromNode(parent) };
            }
            default:
                return undefined;
        }
    }

    // The type of a function type node has a symbol at that node, but it's better to use the symbol for a parameter or type alias.
    function chooseBetterSymbol(s: Symbol): Symbol {
        return s.name === InternalSymbolName.Type
            ? firstDefined(s.declarations, d => isFunctionTypeNode(d) ? d.parent.symbol : undefined) || s
            : s;
    }

    function getArgumentIndex(argumentsList: Node, node: Node) {
        // The list we got back can include commas.  In the presence of errors it may
        // also just have nodes without commas.  For example "Foo(a b c)" will have 3
        // args without commas. We want to find what index we're at.  So we count
        // forward until we hit ourselves, only incrementing the index if it isn't a
        // comma.
        //
        // Note: the subtlety around trailing commas (in getArgumentCount) does not apply
        // here.  That's because we're only walking forward until we hit the node we're
        // on.  In that case, even if we're after the trailing comma, we'll still see
        // that trailing comma in the list, and we'll have generated the appropriate
        // arg index.
        let argumentIndex = 0;
        for (const child of argumentsList.getChildren()) {
            if (child === node) {
                break;
            }
            if (child.kind !== SyntaxKind.CommaToken) {
                argumentIndex++;
            }
        }

        return argumentIndex;
    }

    function getArgumentCount(argumentsList: Node) {
        // The argument count for a list is normally the number of non-comma children it has.
        // For example, if you have "Foo(a,b)" then there will be three children of the arg
        // list 'a' '<comma>' 'b'.  So, in this case the arg count will be 2.  However, there
        // is a small subtlety.  If you have "Foo(a,)", then the child list will just have
        // 'a' '<comma>'.  So, in the case where the last child is a comma, we increase the
        // arg count by one to compensate.
        //
        // Note: this subtlety only applies to the last comma.  If you had "Foo(a,," then
        // we'll have: 'a' '<comma>' '<missing>'
        // That will give us 2 non-commas.  We then add one for the last comma, giving us an
        // arg count of 3.
        const listChildren = argumentsList.getChildren();

        let argumentCount = countWhere(listChildren, arg => arg.kind !== SyntaxKind.CommaToken);
        if (listChildren.length > 0 && last(listChildren).kind === SyntaxKind.CommaToken) {
            argumentCount++;
        }

        return argumentCount;
    }


    function getApplicableSpanForArguments(argumentsList: Node, sourceFile: SourceFile): TextSpan {
        // We use full start and skip trivia on the end because we want to include trivia on
        // both sides. For example,
        //
        //    foo(   /*comment */     a, b, c      /*comment*/     )
        //        |                                               |
        //
        // The applicable span is from the first bar to the second bar (inclusive,
        // but not including parentheses)
        const applicableSpanStart = argumentsList.getFullStart();
        const applicableSpanEnd = skipTrivia(sourceFile.text, argumentsList.getEnd(), /*stopAfterLineBreak*/ false);
        return createTextSpan(applicableSpanStart, applicableSpanEnd - applicableSpanStart);
    }


    function getContainingArgumentInfo(node: Node, position: number, sourceFile: SourceFile, checker: TypeChecker, isManuallyInvoked: boolean): ArgumentListInfo | undefined {
        for (let n = node; !isSourceFile(n) && (isManuallyInvoked || !isBlock(n)); n = n.parent) {
            // If the node is not a subspan of its parent, this is a big problem.
            // There have been crashes that might be caused by this violation.
            Debug.assert(rangeContainsRange(n.parent, n), "Not a subspan", () => `Child: ${Debug.formatSyntaxKind(n.kind)}, parent: ${Debug.formatSyntaxKind(n.parent.kind)}`);
            const argumentInfo = getImmediatelyContainingArgumentOrContextualParameterInfo(n, position, sourceFile, checker);
            if (argumentInfo) {
                return argumentInfo;
            }
        }
        return undefined;
    }

    function getChildListThatStartsWithOpenerToken(parent: Node, openerToken: Node, sourceFile: SourceFile): Node {
        const children = parent.getChildren(sourceFile);
        const indexOfOpenerToken = children.indexOf(openerToken);
        Debug.assert(indexOfOpenerToken >= 0 && children.length > indexOfOpenerToken + 1);
        return children[indexOfOpenerToken + 1];
    }

    function getExpressionFromInvocation(invocation: CallInvocation | TypeArgsInvocation): Expression {
        return invocation.kind === InvocationKind.Call ? getInvokedExpression(invocation.node) : invocation.called;
    }

    function getEnclosingDeclarationFromInvocation(invocation: Invocation): Node {
        return invocation.kind === InvocationKind.Call ? invocation.node : invocation.kind === InvocationKind.TypeArgs ? invocation.called : invocation.node;
    }

    const signatureHelpNodeBuilderFlags = NodeBuilderFlags.OmitParameterModifiers | NodeBuilderFlags.IgnoreErrors | NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope;
    function createSignatureHelpItems(
        candidates: readonly Signature[],
        resolvedSignature: Signature,
        { isTypeParameterList, argumentCount, argumentsSpan: applicableSpan, invocation, argumentIndex }: ArgumentListInfo,
        sourceFile: SourceFile,
        typeChecker: TypeChecker,
    ): SignatureHelpItems {
        const enclosingDeclaration = getEnclosingDeclarationFromInvocation(invocation);
        const callTargetSymbol = invocation.kind === InvocationKind.Contextual ? invocation.symbol : typeChecker.getSymbolAtLocation(getExpressionFromInvocation(invocation));
        const callTargetDisplayParts = callTargetSymbol ? symbolToDisplayParts(typeChecker, callTargetSymbol, /*enclosingDeclaration*/ undefined, /*meaning*/ undefined) : emptyArray;
        const items = candidates.map(candidateSignature => getSignatureHelpItem(candidateSignature, callTargetDisplayParts, isTypeParameterList, typeChecker, enclosingDeclaration, sourceFile));

        if (argumentIndex !== 0) {
            Debug.assertLessThan(argumentIndex, argumentCount);
        }

        const selectedItemIndex = candidates.indexOf(resolvedSignature);
        Debug.assert(selectedItemIndex !== -1); // If candidates is non-empty it should always include bestSignature. We check for an empty candidates before calling this function.

        return { items, applicableSpan, selectedItemIndex, argumentIndex, argumentCount };
    }

    function createTypeHelpItems(
        symbol: Symbol,
        { argumentCount, argumentsSpan: applicableSpan, invocation, argumentIndex }: ArgumentListInfo,
        sourceFile: SourceFile,
        checker: TypeChecker
    ): SignatureHelpItems | undefined {
        const typeParameters = checker.getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol);
        if (!typeParameters) return undefined;
        const items = [getTypeHelpItem(symbol, typeParameters, checker, getEnclosingDeclarationFromInvocation(invocation), sourceFile)];
        return { items, applicableSpan, selectedItemIndex: 0, argumentIndex, argumentCount };
    }

    function getTypeHelpItem(symbol: Symbol, typeParameters: readonly TypeParameter[], checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile): SignatureHelpItem {
        const typeSymbolDisplay = symbolToDisplayParts(checker, symbol);

        const printer = createPrinter({ removeComments: true });
        const parameters = typeParameters.map(t => createSignatureHelpParameterForTypeParameter(t, checker, enclosingDeclaration, sourceFile, printer));

        const documentation = symbol.getDocumentationComment(checker);
        const tags = symbol.getJsDocTags();
        const prefixDisplayParts = [...typeSymbolDisplay, punctuationPart(SyntaxKind.LessThanToken)];
        return { isVariadic: false, prefixDisplayParts, suffixDisplayParts: [punctuationPart(SyntaxKind.GreaterThanToken)], separatorDisplayParts, parameters, documentation, tags };
    }

    const separatorDisplayParts: SymbolDisplayPart[] = [punctuationPart(SyntaxKind.CommaToken), spacePart()];

    function getSignatureHelpItem(candidateSignature: Signature, callTargetDisplayParts: readonly SymbolDisplayPart[], isTypeParameterList: boolean, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile): SignatureHelpItem {
        const { isVariadic, parameters, prefix, suffix } = (isTypeParameterList ? itemInfoForTypeParameters : itemInfoForParameters)(candidateSignature, checker, enclosingDeclaration, sourceFile);
        const prefixDisplayParts = [...callTargetDisplayParts, ...prefix];
        const suffixDisplayParts = [...suffix, ...returnTypeToDisplayParts(candidateSignature, enclosingDeclaration, checker)];
        const documentation = candidateSignature.getDocumentationComment(checker);
        const tags = candidateSignature.getJsDocTags();
        return { isVariadic, prefixDisplayParts, suffixDisplayParts, separatorDisplayParts, parameters, documentation, tags };
    }

    function returnTypeToDisplayParts(candidateSignature: Signature, enclosingDeclaration: Node, checker: TypeChecker): readonly SymbolDisplayPart[] {
        return mapToDisplayParts(writer => {
            writer.writePunctuation(":");
            writer.writeSpace(" ");
            checker.writeType(checker.getReturnTypeOfSignature(candidateSignature), enclosingDeclaration, /*flags*/ undefined, writer);
        });
    }

    interface SignatureHelpItemInfo { readonly isVariadic: boolean; readonly parameters: SignatureHelpParameter[]; readonly prefix: readonly SymbolDisplayPart[]; readonly suffix: readonly SymbolDisplayPart[]; }

    function itemInfoForTypeParameters(candidateSignature: Signature, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile): SignatureHelpItemInfo {
        const typeParameters = (candidateSignature.target || candidateSignature).typeParameters;
        const printer = createPrinter({ removeComments: true });
        const parameters = (typeParameters || emptyArray).map(t => createSignatureHelpParameterForTypeParameter(t, checker, enclosingDeclaration, sourceFile, printer));
        const parameterParts = mapToDisplayParts(writer => {
            const thisParameter = candidateSignature.thisParameter ? [checker.symbolToParameterDeclaration(candidateSignature.thisParameter, enclosingDeclaration, signatureHelpNodeBuilderFlags)!] : [];
            const params = createNodeArray([...thisParameter, ...checker.getExpandedParameters(candidateSignature).map(param => checker.symbolToParameterDeclaration(param, enclosingDeclaration, signatureHelpNodeBuilderFlags)!)]);
            printer.writeList(ListFormat.CallExpressionArguments, params, sourceFile, writer);
        });
        return { isVariadic: false, parameters, prefix: [punctuationPart(SyntaxKind.LessThanToken)], suffix: [punctuationPart(SyntaxKind.GreaterThanToken), ...parameterParts] };
    }

    function itemInfoForParameters(candidateSignature: Signature, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile): SignatureHelpItemInfo {
        const isVariadic = checker.hasEffectiveRestParameter(candidateSignature);
        const printer = createPrinter({ removeComments: true });
        const typeParameterParts = mapToDisplayParts(writer => {
            if (candidateSignature.typeParameters && candidateSignature.typeParameters.length) {
                const args = createNodeArray(candidateSignature.typeParameters.map(p => checker.typeParameterToDeclaration(p, enclosingDeclaration)!));
                printer.writeList(ListFormat.TypeParameters, args, sourceFile, writer);
            }
        });
        const parameters = checker.getExpandedParameters(candidateSignature).map(p => createSignatureHelpParameterForParameter(p, checker, enclosingDeclaration, sourceFile, printer));
        return { isVariadic, parameters, prefix: [...typeParameterParts, punctuationPart(SyntaxKind.OpenParenToken)], suffix: [punctuationPart(SyntaxKind.CloseParenToken)] };
    }

    function createSignatureHelpParameterForParameter(parameter: Symbol, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile, printer: Printer): SignatureHelpParameter {
        const displayParts = mapToDisplayParts(writer => {
            const param = checker.symbolToParameterDeclaration(parameter, enclosingDeclaration, signatureHelpNodeBuilderFlags)!;
            printer.writeNode(EmitHint.Unspecified, param, sourceFile, writer);
        });
        const isOptional = checker.isOptionalParameter(<ParameterDeclaration>parameter.valueDeclaration);
        return { name: parameter.name, documentation: parameter.getDocumentationComment(checker), displayParts, isOptional };
    }

    function createSignatureHelpParameterForTypeParameter(typeParameter: TypeParameter, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile, printer: Printer): SignatureHelpParameter {
        const displayParts = mapToDisplayParts(writer => {
            const param = checker.typeParameterToDeclaration(typeParameter, enclosingDeclaration)!;
            printer.writeNode(EmitHint.Unspecified, param, sourceFile, writer);
        });
        return { name: typeParameter.symbol.name, documentation: typeParameter.symbol.getDocumentationComment(checker), displayParts, isOptional: false };
    }
}
